package dslab.mailbox;

import at.ac.tuwien.dsg.orvell.Shell;
import at.ac.tuwien.dsg.orvell.StopShellException;
import at.ac.tuwien.dsg.orvell.annotation.Command;
import dslab.ComponentFactory;
import dslab.core.Server;
import dslab.enums.Protocol;
import dslab.enums.Role;
import dslab.nameserver.AlreadyRegisteredException;
import dslab.nameserver.INameserverRemote;
import dslab.nameserver.InvalidDomainException;
import dslab.util.Config;
import dslab.util.Message;
import dslab.util.MisconfigurationException;
import dslab.util.WriteTask;

import java.io.InputStream;
import java.io.PrintStream;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.*;
import java.util.concurrent.*;

/**
 * feature complete MailboxServer that implements a shell, DMAP and DMTP;
 * assumes certain configuration keys to be already present, therefore reliant on a factory to handle
 * instantiation
 */
public class MailboxServer implements IMailboxServer {

    // keys that have to be present
    private static final String[] requiredConfigKeys = new String[] {
        "dmtp.tcp.port", "dmap.tcp.port", "domain", "users.config"
    };

    private final String componentId;
    private final Config config;
    private final Config domains;
    private final Config users;
    private final InputStream inputStream;
    private final PrintStream printStream;
    private final BlockingQueue<WriteTask> writeTasks;
    private final Map<String, List<Message>> mails;
    private INameserverRemote root;

    private ExecutorService pool;
    private Server dmtpServer;
    private Server dmapServer;

    /**
     * Creates a new server instance.
     *
     * @param componentId the id of the component that corresponds to the Config resource
     * @param config the component config
     * @param in the input stream to read console input from
     * @param out the output stream to write console output to
     */
    public MailboxServer(String componentId, Config config, Config domains, InputStream in, PrintStream out) throws MisconfigurationException, RemoteException, NotBoundException {
        // print configuration for the benefit of the user
        System.out.printf("@%s: configuration:%n", componentId);
        for (String key : config.listKeys())
            System.out.printf("@%s: { %s: %s }%n", componentId, key, config.getString(key));

        this.componentId = componentId;
        this.config = config;
        this.domains = domains;
        this.inputStream = in;
        this.printStream = out;
        this.writeTasks = new LinkedBlockingQueue<>();
        this.mails = new HashMap<>();

        // check if required keys are present
        for (String key : requiredConfigKeys)
            if (!config.containsKey(key))
                throw new MisconfigurationException();

        // process users as part of mailbox initialization
        users = new Config(config.getString("users.config"));
        for (String key : users.listKeys()) {
            // instantiate emails map
            // (with persistent storage, this would deserialize data on the disk)
            mails.put(key, new ArrayList<>());
            System.out.printf("@%s: { %s: %s }%n", componentId, key, users.getString(key));
        }

        // look up registry for mailbox registration
        // if it fails, fallback to domains.properties
        try {
            this.root = (INameserverRemote) LocateRegistry.getRegistry(
                    config.getString("registry.host"),
                    config.getInt("registry.port"))
                    .lookup(config.getString("root_id"));
        } catch (Exception ignored) {
            this.root = new INameserverRemote() {
                @Override
                public void registerNameserver(String domain, INameserverRemote nameserver) throws RemoteException, AlreadyRegisteredException, InvalidDomainException {

                }

                @Override
                public void registerMailboxServer(String domain, String address) throws RemoteException, AlreadyRegisteredException, InvalidDomainException {

                }

                @Override
                public INameserverRemote getNameserver(String zone) throws RemoteException {
                    return null;
                }

                @Override
                public String lookup(String username) throws RemoteException {
                    return null;
                }

                @Override
                public String lookupDomain(String domainName) throws RemoteException {
                    return domains.getString(domainName);
                }
            };
        }

    }

    @Override
    public void run() {
        try {
            root.registerMailboxServer(config.getString("domain"), "localhost:" + config.getInt("dmtp.tcp.port"));
        } catch (Exception exception) {
            // TODO: make not fatal p.12-13
            // this should not be fatal now, since the remote exception is never thrown
            // when the root nameserver does not exist
            // but need approval
            if (exception instanceof RemoteException) {
                System.err.printf("@%s: a fatal error has occurred: %s", componentId, exception.getMessage());
                return;
            } else if (exception instanceof InvalidDomainException) {
                System.err.printf("@%s: a fatal error has occurred: the following domain was deemed invalid: %s%n", componentId, config.getString("domain"));
                return;
            } else if (exception instanceof AlreadyRegisteredException) {
                System.err.printf("@%s: an error has occurred: Domain is already registered %n", componentId);
            } else {
                System.err.printf("@%s: a fatal error has occurred: %s%n", componentId, exception.getMessage());
            }
        }

        // initialize thread pool to fixed amount related to available cores
        // reasonable estimate for good thread pool thread count
        // minimum of 5 threads required
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        pool = Executors.newFixedThreadPool(availableProcessors < 5 ? 5 : availableProcessors + 1);

        // initialize queue writer thread and add to pool
        Thread writeQueueHandlerThread = new Thread(new WriteQueueHandler(
            String.format("%s-write-queue-handler", componentId),
            writeTasks,
            mails
        ));
        pool.execute(writeQueueHandlerThread);

        // initialize DMTP server thread
        dmtpServer = new Server(
                String.format("%s-dmtp-server", componentId),
                componentId,
                config.getString("domain"),
                config.getInt("dmtp.tcp.port"),
                Role.Mailbox,
                Protocol.DMTP,
                users,
                pool,
                writeTasks,
                mails
        );
        pool.execute(dmtpServer);

        // initialize DMAP server thread
        dmapServer = new Server(
            String.format("%s-dmap-server", componentId),
            componentId,
            config.getString("domain"),
            config.getInt("dmap.tcp.port"),
            Role.Mailbox,
            Protocol.DMAP,
            users,
            pool,
            writeTasks,
            mails
        );
        pool.execute(dmapServer);

        // finally instantiate shell
        Shell shell = new Shell(inputStream, printStream);
        shell.register(this);
        shell.setPrompt(String.format("%s> ", componentId));
        shell.run();
    }

    @Override
    @Command
    public void shutdown() throws StopShellException {
        // inspired by the oracle docs of ExecutorService
        if (pool != null) {
            // attempt closing the dmtp/dmap server thread
            if (dmtpServer != null)
                dmtpServer.shutdown();
            if (dmapServer != null)
                dmapServer.shutdown();

            // prevent new tasks from being submitted
            pool.shutdown();

            // try to await termination of currently active threads and optionally force termination
            try {
                if (!pool.awaitTermination(3, TimeUnit.SECONDS))
                    pool.shutdownNow();
            } catch (InterruptedException exception) {
                // force termination
                pool.shutdownNow();
            }
        }

        System.out.printf("@%s: stopped%n", componentId);

        // close shell
        throw new StopShellException();
    }

    public static void main(String[] args) throws Exception {
        try {
            IMailboxServer server = ComponentFactory.createMailboxServer(args[0], System.in, System.out);
            server.run();
        } catch (MisconfigurationException exception) {
            System.err.printf("@%s: a fatal error has occurred: configuration is lacking one of the following configuration options: %s%n", args[0], Arrays.toString(requiredConfigKeys));
        }
    }
}
